/**************************************************************************************
*
*	Copyright (C) 2010 Brent Andrew O. Chua - See LICENSE.txt for more info.
*		
*	Filename	-	Texture.cpp
*
*	Comments	-	See Texture.h
*
**************************************************************************************/


#include "../Include/Texture.h"
#include "../Include/FileSystem.h"
#include "../Include/ResourceCache.h"
#include "../Include/Engine.h"
#include "../Include/Renderer.h"
#include "../Include/Math.h"
#include "../Include/Vector4.h"
#include "../Include/DX9Mapping.h"

#ifdef __PSX_WINDOWS_WINDOW

#include <d3d9.h>
#include <D3dx9tex.h>

#if defined( PSX_PLATFORM_WINDOWS_WINDOW  )

namespace Pulse
{

	Texture::Texture( void )
	{
		Initialize();
	}

	Texture::~Texture( void )
	{

	}
		
	BOOL Texture::CreateResource( void )
	{
		if ( IsResourceCreated() )
		{
			PSX_PushWarning( "Resource has already been created." );
			return FALSE;
		}

		Initialize();
		SetFlagCreated();
		SetFlagDisabled();
		return TRUE;
	}
	
	BOOL Texture::DestroyResource( void )
	{
		if ( IsResourceCreated() )
		{
			DisableResource();
			Cleanup();
			SetFlagUnloaded();
			SetFlagDestroyed();
		}

		return TRUE;
	}
	
	BOOL Texture::DisableResource( void )
	{
		if ( !IsResourceCreated() )
		{
			PSX_PushWarning( "Resource has not been created." );
			return FALSE;
		}

		Cleanup();
		SetFlagDisabled();
		return TRUE;
	}
	
	BOOL Texture::RestoreResource( void )
	{
		if ( !IsResourceCreated() )
		{
			PSX_PushWarning( "Resource has not been created." );
			return FALSE;
		}

		if ( m_textureDesc.usage != ETextureUsage::NORMAL )
		{
			if ( m_textureDesc.filename.IsEmpty() )
			{
				if ( m_scaleWidth > 0.0f && m_scaleHeight > 0.0f && m_textureDesc.usage == ETextureUsage::RENDERTARGET )
				{
					DisplayMode *pMode = Renderer::GetInstance()->GetDisplayMode();
					m_textureDesc.width = (UINT)(pMode->width * m_scaleWidth);
					m_textureDesc.height = (UINT)(pMode->height * m_scaleHeight);
				}

				Create( m_textureDesc.width, m_textureDesc.height, m_textureDesc.type,
					m_textureDesc.usage, m_textureDesc.format, m_textureDesc.depth );

			}
			else
			{
				CreateFromFile( m_textureDesc.filename.GetCString(), m_textureDesc.type,
					m_textureDesc.usage, m_textureDesc.format );
			}
		}

		SetFlagRestored();
		return TRUE;
	}

	BOOL Texture::LoadResource( const CHAR *pFilename )
	{
		if ( !pFilename )
		{
			const String *pName = GetResourceName();

			if ( pName )
			{
				pFilename = pName->GetCString();
			}
		}

		if ( !pFilename )
		{
			PSX_PushError( "Invalid texture filename." );
			return FALSE;
		}

		if ( IsResourceLoaded() )
		{
			DestroyResource();
			CreateResource();
		}

		return CreateFromFile( pFilename ) == EErrorCode::NONE;
	}
	
	BOOL Texture::SaveResource( const CHAR *pFilename )
	{
		// TODO: Implement this method...
		PSX_PushError( "DirectX 9 texture SaveResource() method not implemented." );
		return FALSE;
	}

	void Texture::Initialize( void )
	{
		new( &m_textureDesc ) TextureDescription();
		m_scaleWidth		= 1.0f;
		m_scaleHeight		= 1.0f;
		m_pDevice			= NULL;
		m_pRTDesc			= NULL;
		m_pBackBuffer		= NULL;
		m_pSurface			= NULL;
		m_textureUnit		= 0;
		m_bBoundAsTexture	= FALSE;

		m_lockedArea.pRect = NULL;
	}

	void Texture::Cleanup( void )
	{
		PSX_SafeRelease( m_pSurface );
		PSX_SafeRelease( m_pBackBuffer );

		switch( m_textureDesc.type )
		{
		case ETextureType::_1D:
		case ETextureType::_2D:
			PSX_SafeRelease( m_p2DTexture );
			PSX_SafeDelete( m_lockedArea.pRect );
			break;
		case ETextureType::_3D:
			PSX_SafeRelease( m_p3DTexture );
			PSX_SafeDelete( m_lockedArea.pBox );
			break;
		case ETextureType::_CUBE:
			PSX_SafeRelease( m_pCubeTexture );
			PSX_SafeDelete( m_lockedArea.pRect );
			break;
		}

		PSX_SafeDelete( m_pRTDesc );
	}

	EErrorCode::Type Texture::Create( UINT width, UINT height, ETextureType::Type type, 
		ETextureUsage::Type usage, ETextureFormat::Type format, UINT depth )
	{
		PSX_PushLog3( "Creating empty texture." );

		// Fill the texture description surface
		m_textureDesc.format	= format;
		m_textureDesc.type		= type;
		m_textureDesc.usage		= usage;
		m_textureDesc.width		= width;
		m_textureDesc.height	= height;
		m_textureDesc.depth		= depth;
		m_textureDesc.filename.Clear();

		// Specify DirectX settings
		DWORD _usage = 0;
		D3DPOOL pool = D3DPOOL_MANAGED;
		UINT numMipMaps = 0;

		// We change from "full-chain" only if texture is dynamic or serves as
		//	a render target
		GetUsagePoolMipMaps( _usage , reinterpret_cast<DWORD&>(pool), numMipMaps );

		D3DFORMAT _format = GetFormat( format, _usage );

		m_pDevice = (IDirect3DDevice9*)Renderer::GetInstance()->GetDevice()->GetDevice();

		HRESULT hRes = 0;
		switch( type )
		{
		case ETextureType::_1D:
			hRes = D3DXCreateTexture( m_pDevice, width, 1, numMipMaps, _usage, 
				_format, pool, &m_p2DTexture );
			break;
		case ETextureType::_2D:
			hRes = D3DXCreateTexture( m_pDevice, width, height, numMipMaps, _usage,
				_format, pool, &m_p2DTexture );
			break;
		case ETextureType::_3D:
			hRes = D3DXCreateVolumeTexture( m_pDevice, width, height, depth, 
				numMipMaps, _usage, _format, pool, &m_p3DTexture );
			break;
		case ETextureType::_CUBE:
			hRes = D3DXCreateCubeTexture( m_pDevice, width, numMipMaps, _usage, 
				_format, pool, &m_pCubeTexture );
			break;
		}

		if ( hRes == D3D_OK )
		{
			if ( type != ETextureType::_3D )
				m_lockedArea.pRect = new D3DLOCKED_RECT;
			else
				m_lockedArea.pBox = new D3DLOCKED_BOX;
		}
		else
		{
			m_p2DTexture = NULL;	
			return EErrorCode::GRAPHICS;
		}

		if ( usage == ETextureUsage::RENDERTARGET )
			CreateRenderTarget( m_pDevice );

		SetFlagLoaded();
		SetFlagRestored();

		return EErrorCode::NONE;
	}

	EErrorCode::Type Texture::CreateFromFile( const CHAR *pFilename, ETextureType::Type type, 
		ETextureUsage::Type usage, ETextureFormat::Type format )
	{
		PSX_PushLog3( "Creating texture from file." );

		// Fill texture description surface
		m_textureDesc.format	= format;
		m_textureDesc.type		= type;
		m_textureDesc.usage		= usage;
		m_textureDesc.filename	= pFilename;

		// Specify DX settings
		DWORD	_usage		= 0;
		D3DPOOL pool		= D3DPOOL_MANAGED;
		UINT	numMipMaps	= 0;

		// Change to full-chain if it is dynamic or used as a render target
		GetUsagePoolMipMaps( _usage, (DWORD&)pool, numMipMaps );

		D3DFORMAT _format = GetFormat( format, _usage );

		// Load texture file
		FileHandlePtr hFile = FileSystem::GetInstance()->OpenFile( pFilename );

		if ( !hFile->IsValid() )
		{
			PSX_PushError( "Failed to load texture file %s.", pFilename );
			return EErrorCode::GRAPHICS;
		}

		ResHandlePtr hResource =  Engine::GetInstance()->GetResourceCache()->GetHandle( hFile );

		m_pDevice = (IDirect3DDevice9*)Renderer::GetInstance()->GetDevice()->GetDevice();

		// Create texture
		HRESULT hr = 0;
		
		switch( type )
		{
		case ETextureType::_1D:
			hr = D3DXCreateTextureFromFileInMemoryEx( m_pDevice, hResource->GetBuffer(), 
				hResource->GetSize(), 0, 1, numMipMaps, _usage, _format, pool, D3DX_DEFAULT, 
				D3DX_DEFAULT, 0, NULL, NULL, &m_p2DTexture );
			break;
		case ETextureType::_2D:
			hr = D3DXCreateTextureFromFileInMemoryEx( m_pDevice, hResource->GetBuffer(),
				hResource->GetSize(), 0, 0, numMipMaps, _usage, _format, pool, D3DX_DEFAULT,
				D3DX_DEFAULT, 0, NULL, NULL, &m_p2DTexture );
			break;
		case ETextureType::_3D:
			hr = D3DXCreateVolumeTextureFromFileInMemoryEx( m_pDevice, hResource->GetBuffer(),
				hResource->GetSize(), 0, 0, 0, numMipMaps, _usage, _format, pool, D3DX_DEFAULT,
				0, 0, NULL, NULL, &m_p3DTexture );
			break;
		case ETextureType::_CUBE:
			hr = D3DXCreateCubeTextureFromFileInMemoryEx( m_pDevice, hResource->GetBuffer(),
				hResource->GetSize(), 0, numMipMaps, _usage, _format, pool, D3DX_DEFAULT,
				0, 0, NULL, NULL, &m_pCubeTexture );
			break;
		}

		if ( hr == D3D_OK )
		{
			if ( type == ETextureType::_1D || type == ETextureType::_2D || type == ETextureType::_CUBE )
			{
				D3DSURFACE_DESC desc;

				if ( type == ETextureType::_CUBE )
					m_pCubeTexture->GetLevelDesc( 0, &desc );
				else
					m_p2DTexture->GetLevelDesc( 0, &desc );
				
				m_textureDesc.width = desc.Width;
				m_textureDesc.height = desc.Height;

				m_lockedArea.pRect = new D3DLOCKED_RECT;
			}
			else // 3D texture
			{
				D3DVOLUME_DESC desc;
				m_p3DTexture->GetLevelDesc( 0, &desc );
				m_textureDesc.width = desc.Width;
				m_textureDesc.height = desc.Height;
			
				m_lockedArea.pBox = new D3DLOCKED_BOX;
			}

			if ( usage == ETextureUsage::RENDERTARGET )
				CreateRenderTarget( m_pDevice );
		}
		else
		{
			m_p2DTexture = PSX_NULL;
			PSX_PushError( "Failed to load texture file %s.", pFilename );
			return EErrorCode::GRAPHICS;
		}

		SetFlagLoaded();
		SetFlagRestored();

		return EErrorCode::NONE;
	}


	void Texture::Bind( UINT textureUnit, BOOL bForceSet, DWORD clearFlags )
	{
		// If currently bound as texture
		if ( m_bBoundAsTexture && !bForceSet )
		{
			IDirect3DBaseTexture9 *pTexture;
			m_pDevice->GetTexture( textureUnit, &pTexture );
			if ( pTexture == m_p2DTexture )
				NormalUnbind();

			PSX_SafeRelease( pTexture );
			m_bBoundAsTexture = FALSE;
		}

		m_textureUnit = textureUnit;
		m_bIsForced = bForceSet;

		if ( bForceSet )
		{
			NormalBind();
		}
		else
		{
			switch( m_textureDesc.usage )
			{
				case ETextureUsage::NORMAL:
					NormalBind();
					break;
				case ETextureUsage::RENDERTARGET:
					BindRenderTarget( clearFlags );
					break;
			}
		}
	}

	void Texture::Unbind( void )
	{
		if ( m_bIsForced )
		{
			switch( m_textureDesc.usage )
			{
			case ETextureUsage::RENDERTARGET:
				UnbindRenderTarget();
				break;
			}
		}
		else
		{
			switch( m_textureDesc.usage )
			{
			case ETextureUsage::RENDERTARGET:
				UnbindRenderTarget();
				break;
			}
		}
	}

	void Texture::SetData( void *pData, UINT level ) const
	{
		if ( !Lock( level ) )
			return;

		if ( m_textureDesc.type == ETextureType::_3D )
		{
			DWORD bytes = m_lockedArea.pBox->RowPitch * m_lockedArea.pBox->SlicePitch;
			PSX_MemCopy( m_lockedArea.pBox->pBits, pData, bytes );
		}
		else
		{
			DWORD bytes = m_lockedArea.pRect->Pitch * m_textureDesc.height;
			PSX_MemCopy( m_lockedArea.pRect->pBits, pData, bytes );
		}

		switch( m_textureDesc.type )
		{
		case ETextureType::_1D:
		case ETextureType::_2D:
			m_p2DTexture->UnlockRect( level );
			break;
		case ETextureType::_3D:
			m_p3DTexture->UnlockBox( level );
			break;
		case ETextureType::_CUBE:
			m_pCubeTexture->UnlockRect( static_cast<D3DCUBEMAP_FACES>(0), level );
			break;
		}
	}
	
	void Texture::SetDataFloat( void *pData, UINT size, UINT level ) const
	{
		if ( !Lock( level ) )
			return;

		if ( m_textureDesc.type == ETextureType::_3D )
		{
			PSX_MemCopy( m_lockedArea.pBox->pBits, pData, size );
		}
		else
		{
			BYTE *pBytes = (BYTE*)m_lockedArea.pRect->pBits;
			Vector4 *_pData = (Vector4*)pData;
			INT bitDepth = m_lockedArea.pRect->Pitch / m_textureDesc.width;

			UINT ii = 0;
			for ( UINT y = 0; y < m_textureDesc.height; ++y, ++ii )
			{
				// Copy one line at a time
				PSX_MemCopy( &pBytes[ y * m_lockedArea.pRect->Pitch], &_pData[ii],sizeof(Vector4) * m_textureDesc.width );
			}
		}

		switch( m_textureDesc.type )
		{
		case ETextureType::_1D:
		case ETextureType::_2D:
			m_p2DTexture->UnlockRect( level );
			break;
		case ETextureType::_3D:
			m_p3DTexture->UnlockBox( level );
			break;
		case ETextureType::_CUBE:
			m_pCubeTexture->UnlockRect( static_cast<D3DCUBEMAP_FACES>(0), level );
			break;
		}
	}

	void * Texture::GetData( UINT level ) const
	{
		return NULL;
	}

	void Texture::ClearRendertarget( void )
	{
		if ( m_textureDesc.usage == ETextureUsage::RENDERTARGET )
		{
			BindRenderTarget( EClearBufferType::RENDER_TARGET );
			UnbindRenderTarget();
		}
	}

	void Texture::NormalBind( void )
	{
		switch( m_textureDesc.type )
		{
		case ETextureType::_1D:
			m_pDevice->SetTexture( m_textureUnit, m_p2DTexture );
			break;
		case ETextureType::_2D:
			m_pDevice->SetTexture( m_textureUnit, m_p2DTexture );
			break;
		case ETextureType::_3D:
			m_pDevice->SetTexture( m_textureUnit, m_p3DTexture );
			break;
		case ETextureType::_CUBE:
			m_pDevice->SetTexture( m_textureUnit, m_pCubeTexture );
			break;
		}

		if ( m_textureDesc.usage == ETextureUsage::RENDERTARGET )
			m_bBoundAsTexture = TRUE;
	}
	
	void Texture::BindRenderTarget( DWORD clearFlags )
	{
		if ( !m_textureUnit )
			m_pDevice->GetRenderTarget( m_textureUnit, &m_pBackBuffer );

		// Do we need to change our viewport?
		if ( m_pRTDesc->bViewportChange )
		{	
			// Save out old depth and viewport
			m_pDevice->GetDepthStencilSurface( &m_pRTDesc->pOldZBuffer );
			m_pDevice->GetViewport( &m_pRTDesc->oldViewport );
		}

		m_p2DTexture->GetSurfaceLevel( 0, &m_pSurface );

		// Check if the render target index is valid for this device
		UINT target = m_textureUnit;
		UINT maxRTs = 0;
		Renderer::GetInstance()->GetDevice()->TestCapabilities( EGraphicsCaps::MAX_RTS, &maxRTs );
		target = Math::Clamp( target, (UINT)0, maxRTs );

		// change viewport
		if ( m_pRTDesc->bViewportChange && !target )
		{
			m_pDevice->SetViewport( &m_pRTDesc->viewport );

			// Change depth stencil surface
			if ( clearFlags & EClearBufferType::RENDER_TARGET )
				m_pDevice->SetDepthStencilSurface( m_pRTDesc->pZBuffer );
			else
				m_pDevice->SetDepthStencilSurface( NULL );
		}

		m_pDevice->SetRenderTarget( target, m_pSurface );

		// TODO: Reset scissor rectangle
		//Renderer::GetInstance()->SetScissorRectangle(...);

		if ( clearFlags & EClearBufferType::RENDER_TARGET || clearFlags & EClearBufferType::DEPTH )
			Renderer::GetInstance()->ClearBuffer( clearFlags );
	}
	
	void Texture::NormalUnbind( void )
	{
		m_pDevice->SetTexture( m_textureUnit, NULL );
	}
	
	void Texture::UnbindRenderTarget( void )
	{
		if ( !m_pBackBuffer && !m_textureUnit )
			return;

		Renderer *pRenderer = Renderer::GetInstance();

		UINT target = m_textureUnit;
		UINT maxRTs = 0;
		Renderer::GetInstance()->GetDevice()->TestCapabilities( EGraphicsCaps::MAX_RTS, &maxRTs );
		target = Math::Clamp( target, (UINT)0, maxRTs );

		m_pDevice->SetRenderTarget( target, m_pBackBuffer );

		if ( m_pRTDesc->bViewportChange && !target )
		{
			m_pDevice->SetDepthStencilSurface( m_pRTDesc->pOldZBuffer );
			m_pDevice->SetViewport( &m_pRTDesc->oldViewport );
		}

		// Release surfaces
		PSX_SafeRelease( m_pSurface );
		PSX_SafeRelease( m_pBackBuffer );
		PSX_SafeRelease( m_pRTDesc->pOldZBuffer );
	}
	
	void Texture::GetUsagePoolMipMaps( DWORD &dxUsage, DWORD & dxPool, UINT &mips )
	{
		if ( m_textureDesc.usage == ETextureUsage::RENDERTARGET )
		{
			dxUsage = D3DUSAGE_RENDERTARGET;
			dxPool = 0;
			mips = 1;
		}
		else if ( m_textureDesc.usage == ETextureUsage::DYNAMIC )
		{
			dxUsage = D3DUSAGE_DYNAMIC;
			dxPool = 0;
			mips = 1;
		}
	}

	D3DFORMAT Texture::GetFormat( ETextureFormat::Type &format, DWORD &usage )
	{
		D3DFORMAT d3dFormat = DX9Mapping::ToFormat( format );

		if ( format == ETextureFormat::DEFAULT )
			return d3dFormat;

		D3DRESOURCETYPE resType = D3DRTYPE_TEXTURE;

		switch( m_textureDesc.type )
		{
		case ETextureType::_1D:
		case ETextureType::_2D:
			resType = D3DRTYPE_TEXTURE;
			break;
		case ETextureType::_3D:
			resType = D3DRTYPE_VOLUMETEXTURE;
			break;
		case ETextureType::_CUBE:
			resType = D3DRTYPE_CUBETEXTURE;
			break;
		}

		// Check for device support
		D3DFORMAT adapterFormat = D3DFMT_X8R8G8B8;
		IDirect3D9 *pD3D = Renderer::GetInstance()->GetDevice()->GetD3D();
		if ( FAILED( pD3D->CheckDeviceFormat( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, adapterFormat,
			usage, resType, d3dFormat ) ) )
		{
			PSX_PushWarning( "Texture format %d is not supported by this adapter.", (int)format );
			d3dFormat = D3DFMT_A8R8G8B8;
		}

		return d3dFormat;
	}

	void Texture::CreateRenderTarget( IDirect3DDevice9 *pDevice )
	{
		Renderer *pRenderer = Renderer::GetInstance();
		DisplayMode *pDispMode = pRenderer->GetDisplayMode();
		m_pRTDesc = new RTDESC;

		if ( m_textureDesc.width != pDispMode->width ||
			m_textureDesc.height != pDispMode->height )
		{
			pDevice->CreateDepthStencilSurface( m_textureDesc.width, m_textureDesc.height,
				D3DFMT_D16, D3DMULTISAMPLE_NONE, 0, TRUE, &m_pRTDesc->pZBuffer, NULL );

			m_pRTDesc->viewport.X = 0;
			m_pRTDesc->viewport.Y = 0;
			m_pRTDesc->viewport.Width = m_textureDesc.width;
			m_pRTDesc->viewport.Height = m_textureDesc.height;
			m_pRTDesc->viewport.MinZ = 0.0f;
			m_pRTDesc->viewport.MaxZ = 1.0f;

			m_pRTDesc->bViewportChange = TRUE;
		}
	}

	// Locks texture surface if possible.
	BOOL Texture::Lock( UINT level ) const
	{
		// Render targets are not lockable!
		if ( m_textureDesc.usage == ETextureUsage::RENDERTARGET )
		{
			PSX_PushError( "Locking texture failed. Render targets can't be locked." );
			return FALSE;
		}

		if ( m_textureDesc.type == ETextureType::_1D || m_textureDesc.type == ETextureType::_2D )
		{
			if ( FAILED( m_p2DTexture->LockRect( level, m_lockedArea.pRect, NULL, 0 ) ) )
			{
				PSX_PushError( "Failed to lock 2D texture." );
				return FALSE;
			}
		}
		else if ( m_textureDesc.type == ETextureType::_3D )
		{
			if ( FAILED( m_p3DTexture->LockBox( level, m_lockedArea.pBox, NULL, 0 ) ) )
			{
				PSX_PushError( "Failed to lock 3D texture." );
				return FALSE;
			}
		}
		else if ( m_textureDesc.type == ETextureType::_CUBE )
		{
			if ( FAILED( m_pCubeTexture->LockRect( static_cast<D3DCUBEMAP_FACES>(0), level, m_lockedArea.pRect, NULL, 0 ) ) )
			{
				PSX_PushError( "Failed to lock Cube texture." );
				return FALSE;
			}
		}
		
		return TRUE;
	}

}

#endif /* defined( PSX_PLATFORM_WINDOWS ) */

#endif /* __PSX_WINDOWS_WINDOW */
